// Copyright 2022 Jeroen van Nugteren

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// include header
#include "dfintersect.hh"

// common headers
#include "rat/common/extra.hh"
#include "rat/common/error.hh"

// code specific to Rat
namespace rat{namespace dm{

	// constructor
	DFIntersect::DFIntersect(){
		set_name("Intersect");
	}

	// constructcor
	DFIntersect::DFIntersect(const std::list<ShDistFunPr> &dflist) : DFIntersect(){
		for(auto it=dflist.begin();it!=dflist.end();it++)add_df(*it);
	}

	// constructcor
	DFIntersect::DFIntersect(
		const ShDistFunPr &dfa, 
		const ShDistFunPr &dfb) : DFIntersect(){
		// check user input
		if(dfa==NULL)rat_throw_line("first distance function points to NULL");
		if(dfb==NULL)rat_throw_line("second distance function points to NULL");

		// set to self
		add_df(dfa); add_df(dfb);
	}

	// factory
	ShDFIntersectPr DFIntersect::create(const std::list<ShDistFunPr> &dflist){
		return std::make_shared<DFIntersect>(dflist);
	}


	// factory
	ShDFIntersectPr DFIntersect::create(){
		return std::make_shared<DFIntersect>();
	}

	// factory
	ShDFIntersectPr DFIntersect::create(
		const ShDistFunPr &dfa, 
		const ShDistFunPr &dfb){
		return std::make_shared<DFIntersect>(dfa,dfb);
	}

	// add parts
	void DFIntersect::add(const ShDistFunPr& df){
		add_df(df);
	}
	
	// add distance function
	arma::uword DFIntersect::add_df(const ShDistFunPr &df){
		const arma::uword index = df_.size()+1;
		df_.insert({index,df}); return index;
	}
	
	// retreive distance function at index
	const ShDistFunPr& DFIntersect::get_df(const arma::uword index) const{
		auto it = df_.find(index);
		if(it==df_.end())rat_throw_line("index does not exist");
		return (*it).second;
	}
	
	// delete distance function at index
	bool DFIntersect::delete_df(const arma::uword index){	
		auto it = df_.find(index);
		if(it==df_.end())return false;
		(*it).second = NULL; return true;
	}

	// number of distance functions
	arma::uword DFIntersect::num_df() const{
		return df_.size();
	}

	// re-index nodes after deleting
	void DFIntersect::reindex(){
		std::map<arma::uword, ShDistFunPr> new_df; arma::uword idx = 1;
		for(auto it=df_.begin();it!=df_.end();it++)
			if((*it).second!=NULL)new_df.insert({idx++, (*it).second});
		df_ = new_df;
	}

	// perimeter function
	ShPerimeterPr DFIntersect::create_perimeter(const fltp delem) const{
		std::list<ShPerimeterPr> perimeters;
		for(auto it=df_.begin();it!=df_.end();it++)
			perimeters.push_back((*it).second->create_perimeter(delem));
		return Perimeter::create(perimeters);
	}

	// get bounding box
	arma::Mat<fltp>::fixed<2,2> DFIntersect::get_bounding() const{
		// check input
		is_valid(true);

		// create upper limit for boundary
		arma::Mat<fltp>::fixed<2,2> M{
			-RAT_CONST(1e99),RAT_CONST(1e99),
			-RAT_CONST(1e99),RAT_CONST(1e99)};
	
		// walk over distance functions
		for(auto it=df_.begin();it!=df_.end();it++){
			const arma::Mat<fltp>::fixed<2,2> Mt = (*it).second->get_bounding();
			M.row(0) = arma::max(M.row(0),Mt.row(0));
			M.row(1) = arma::min(M.row(1),Mt.row(1));
		}

		// return box
		return M;
	}

	// get fixed points
	arma::Mat<fltp> DFIntersect::get_fixed(const fltp abstol) const{
		// check validity
		is_valid(true);

		// settings
		// const fltp delta = abstol/2;

		// size of the elements
		const fltp delem = calc_bounding_size()/64;

		// allocate fixed points
		arma::field<arma::Mat<fltp> > pfix_fld((df_.size()*df_.size() - df_.size())/2 + df_.size(),1); arma::uword cnt = 0;

		// walk over other distance functions
		for(auto it1=df_.begin();it1!=df_.end();it1++){
			// get distance function
			const ShDistFunPr& df1 = (*it1).second;

			// get perimeter of main object
			const ShPerimeterPr perimeter = df1->create_perimeter(delem);

			// walk over other objects
			for(auto it2=std::next(it1);it2!=df_.end();it2++){
				// get distance function
				const ShDistFunPr& df2 = (*it2).second;

				// find intersection points
				pfix_fld(cnt) = perimeter->intersect_df(df2);

				// refine their position using the distance functions
				// Perimeter::refine_intersection(pfix_fld(cnt),df1,df2,delta,abstol);

				// increment counter
				cnt++;
			}
		}

		// get fixed points from distance functions themselves
		for(auto it=df_.begin();it!=df_.end();it++)
			pfix_fld(cnt++) = (*it).second->get_fixed(abstol);

		// combine points
		arma::Mat<fltp> pfix = cmn::Extra::field2mat(pfix_fld);

		// find points that are actually on the boundary
		pfix.shed_rows(arma::find(arma::abs(calc_distance(pfix))>abstol));

		// combine points and return
		return pfix;
	}

	// distance function
	arma::Col<fltp> DFIntersect::calc_distance(const arma::Mat<fltp> &p) const{
		arma::Col<fltp> distance = df_.at(1)->calc_distance(p);
		for(auto it=std::next(df_.begin());it!=df_.end();it++)
			distance = arma::max(distance,(*it).second->calc_distance(p));
		return distance;
	}

	// validity check
	bool DFIntersect::is_valid(const bool enable_throws)const{
		// check user input
		if(df_.empty()){if(enable_throws){rat_throw_line("no distance functions set");} return false;};
		for(auto it=df_.begin();it!=df_.end();it++){
			if((*it).second==NULL){if(enable_throws){rat_throw_line("distance function points to NULL");} return false;};
			if(!(*it).second->is_valid(enable_throws))return false;
		}
		return true;
	}

	// type string for serialization
	std::string DFIntersect::get_type(){
		return "rat::dm::dfintersect";
	}

	// method for serialization into json
	void DFIntersect::serialize(Json::Value &js, cmn::SList &list) const{
		// parent
		DistFun::serialize(js,list);

		// store type
		js["type"] = get_type();

		// distance functions
		for(auto it=df_.begin();it!=df_.end();it++)
		 	js["distfuns"].append(cmn::Node::serialize_node((*it).second,list));
	}

	// method for deserialisation from json
	void DFIntersect::deserialize(
		const Json::Value &js, cmn::DSList &list, 
		const cmn::NodeFactoryMap &factory_list, 
		const boost::filesystem::path &pth){
		
		// parent
		DistFun::deserialize(js,list,factory_list,pth);

		// backwards compatibility with v2.015.3
		if(js.isMember("distfuna") && js.isMember("distfunb")){
			add(cmn::Node::deserialize_node<DistFun>(js["distfuna"], list, factory_list, pth));
			add(cmn::Node::deserialize_node<DistFun>(js["distfunb"], list, factory_list, pth));
		}

		// deserialize distance functions
		else{
			for(auto it = js["distfuns"].begin();it!=js["distfuns"].end();it++){
				add_df(cmn::Node::deserialize_node<DistFun>(*it, list, factory_list, pth));
			}
		}
	}

}}